Skip to content

feat(server/s3): support multipart upload - #2813

Draft
xrgzs wants to merge 3 commits into
mainfrom
feat/s3-multipart
Draft

feat(server/s3): support multipart upload#2813
xrgzs wants to merge 3 commits into
mainfrom
feat/s3-multipart

Conversation

@xrgzs

@xrgzs xrgzs commented Jul 21, 2026

Copy link
Copy Markdown
Member

Summary / 摘要

为 fake S3 服务器实现分片上传(multipart upload)支持,并自动回收被遗弃的分片上传。本 PR 含三个提交:

  1. refactor(server/s3): use OpenListTeam/gofakes3 -- 将依赖从 github.com/itsHenry35/gofakes3 v0.0.8 切换到 github.com/OpenListTeam/gofakes3 v0.8.0。旧版只有把所有分片缓存在内存的默认上传器;OpenListTeam fork 新增了可选的 MultipartBackend 接口,允许后端自行流式处理分片,这是实现分片上传的前提。同步更新 server/s3 下 8 个文件的 import 路径。
  2. feat(server/s3): support multipart upload -- 在 s3Backend 上实现 gofakes3.MultipartBackend,把分片上传四个操作(Initiate / UploadPart / Complete / Abort)路由到 OpenList 自有后端:每个分片流式写入本地临时文件,完成时按分片号顺序拼合并写入底层存储。
  3. feat(server/s3): reap abandoned multipart uploads -- 回收被遗弃的分片上传,避免临时文件无限累积。
  • 为什么需要:此前分片上传回退到 gofakes3 默认的内存上传器,会把每个分片都缓存在内存中,大文件分片上传可能耗尽内存;且客户端不 complete/abort 的孤儿上传会一直占用磁盘。现在改为按分片落盘(内存占用恒定),并由后台 reaper 自动清理超时未活动的上传。

  • 用户可感知的行为变化:S3 客户端(awscli / s3cmd / rclone 等)的分片上传现在可用,不再因内存限制而失败;被遗弃的分片上传会在 TTL 后被自动回收。

  • 重要实现变化:

    • 依赖切换到 OpenListTeam/gofakes3 v0.8.0(base Backend 接口兼容,既有单分片 PutObject 等行为不变)
    • 新增 server/s3/multipart.go,实现 gofakes3.MultipartBackend(CreateMultipartUpload / UploadPart / CompleteMultipartUpload / AbortMultipartUpload)
    • s3Backend 新增 uploads sync.Map 跟踪进行中的上传;每个上传记录 lastActivity,在创建与每次分片上传时更新
    • PutObject 主体抽取为可复用的 putStream,与分片 Complete 路径共享(目录创建、元数据、忽略规则一致);单分片 PutObject 行为不变
    • 分片临时文件存放于 conf.Conf.TempDir 下的 s3-multipart-* 目录;返回 S3 规范的分片 etag("<md5ofmd5s>-N")
    • 校验分片升序、etag 匹配、分片存在;短读(实际字节少于声明 Content-Length)返回 ErrIncompleteBody;abort 幂等;Complete 失败时保留上传以便客户端重试
    • 后台 reaper:每个 backend 实例启动一个协程,按 TTL/4(钳制 [10s, 1h])间隔回收 lastActivity 超过 TTL 的上传及其临时目录;启动时额外清理上次进程崩溃残留的 s3-multipart-* 目录(仅清理超过 TTL 的,避免误伤并发启动的兄弟实例)
    • 新增可配置 s3.multipart_ttl(env S3_MULTIPART_TTL),用 time.ParseDuration 解析,空/非法/<=0 回退默认 24h
  • 配置/存储/API/兼容性:新增一个可选配置项 s3.multipart_ttl(默认 24h,不配置即用默认值);不修改已有 API、存储格式或迁移行为。

  • This PR has breaking changes.
    / 此 PR 包含破坏性变更。

  • This PR changes public API, config, storage format, or migration behavior.
    / 此 PR 修改了公开 API、配置、存储格式或迁移行为。

  • This PR requires corresponding changes in related repositories.
    / 此 PR 需要关联仓库同步修改。

Related repository PRs / 关联仓库 PR:

  • OpenList-Frontend: 无
  • OpenList-Docs: TODO

Related Issues / 关联 Issue

Related to #460

Testing / 测试

  • go test ./...
  • Manual test / 手动测试:

执行过的命令与结果(平台:macOS arm64,Go 1.26.5):

  • go build ./server/s3/go build ./server/go build ./internal/conf/ -- 通过
  • go vet ./server/...go vet ./internal/conf/ -- 干净
  • gofmt -l server/s3/*.go internal/conf/config.go -- 干净
  • go test ./server/s3/ -count=1 -- 通过,包含新增测试:
    • TestMultipartUploadEndToEnd:在真实 Local 驱动上 create->3 分片->complete,校验落盘文件内容为拼接结果、etag 为 "<hex>-3";同号重传覆盖;短读->ErrIncompleteBody;未知 upload->ErrNoSuchUpload;越界分片号->ErrInvalidPart;乱序->ErrInvalidPartOrder;错 etag->ErrInvalidPart;缺分片->ErrInvalidPart;失败的 complete 保留上传可重试
    • TestMultipartAbort:abort 删除临时目录并移除记录,且对未知 upload 幂等
    • TestMultipartReapExpired:lastActivity 超过 TTL 的上传被回收且临时目录删除,活跃上传保留
    • TestMultipartCleanupStaleDirs:超过 TTL 的 s3-multipart-* 残留目录被清理,新目录保留
  • go test ./...:server/s3 通过。其余失败均与本次改动无关且为环境/既有问题--
    • pkg/aria2/rpc:需连接 localhost:6800 的 aria2 守护进程(本机禁网)
    • drivers/teldrivedrivers/webdav:需外部服务器/凭据
    • internal/net TestNewOSSClientUsesEnvironmentHTTPSProxy:仓库既有失败(expected *http.Transport, got *net.safeTransport)
    • 部分 drivers/*internal/offline_download/* 的 build failed:并行 go test ./... 时模块缓存写入竞争所致,单独 go build 正常
    • 上述包均不导入 server/s3(仅 server/s3.go 导入),不受本次改动影响

未执行:对运行中的 OpenList 实例用 awscli / rclone 做手动联调(如维护者需要可补)。

Checklist / 检查清单

  • I have read CONTRIBUTING.
    / 我已阅读 CONTRIBUTING
  • I confirm this contribution follows the repository license, contribution policy, and code of conduct.
    / 我确认此贡献符合仓库许可证、贡献规范和行为准则。
  • I have formatted the changed code with gofmt, go fmt, or prettier where applicable.
    / 我已按适用情况使用 gofmtgo fmtprettier 格式化变更代码。
  • I have requested review from relevant maintainers or code owners where applicable.
    / 我已在适用情况下请求相关维护者或代码所有者审查。

AI Disclosure / AI 使用声明

  • This PR includes AI-assisted content.
    / 此 PR 包含 AI 辅助内容。

Tools used / 使用工具:

  • ChatGPT
  • Codex + GLM 5.2
  • GitHub Copilot
  • Claude
  • Gemini
  • Other (please specify) / 其他(请注明):

Usage scope / 使用范围:

  • Code generation / 代码生成

  • Refactoring / 重构

  • Documentation / 文档

  • Tests / 测试

  • Translation / 翻译

  • Review assistance / 审查辅助

  • I have reviewed and validated all AI-assisted content included in this PR.
    / 我已审核并验证此 PR 中的所有 AI 辅助内容。

  • I have ensured that all AI-assisted commits include Co-Authored-By attribution.
    / 我已确保所有 AI 辅助提交都包含 Co-Authored-By 归属信息。

  • I can reproduce all AI-assisted content included in this PR without any AI tools.
    / 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。

Known Limitations / 已知限制

  • 分片临时文件写入 conf.Conf.TempDir,大文件上传会占用等量本地磁盘(相对原内存缓冲已是改进,为流式后端的固有取舍)。
  • TTL 内的崩溃残留目录会在下次启动且超过 TTL 后才被清理(默认 24h);正常运行期间的孤儿上传由后台 reaper 在 TTL 后自动回收。

@xrgzs xrgzs added WIP An Issue already has a PR to fix Module: Server API and protocol changes Module: Stream Transmission optimization and file stream handling-related features ecosystem labels Jul 21, 2026
xrgzs and others added 3 commits August 7, 2026 09:14
Replace itsHenry35/gofakes3 with OpenListTeam/gofakes3

Signed-off-by: MadDogOwner <xiaoran@xrgzs.top>
- Implement gofakes3 MultipartBackend (Create/UploadPart/Complete/Abort)
  on s3Backend so multipart parts stream to local temp files instead of
  being buffered in memory
- Track in-progress uploads via a new uploads sync.Map on s3Backend
- Refactor the PutObject body into a reusable putStream helper shared with
  the multipart Complete path
- Validate part ordering, etags and existence, reject short reads, and
  return S3-style multipart etags
- Add end-to-end multipart tests against a real Local driver

Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
- Track lastActivity on each multipart upload, updated on create and
  every part upload, so idle uploads can be detected
- Add a background reaper per backend instance that removes uploads
  inactive for longer than the TTL (default 24h) and cleans their temp
  directories
- Add a startup sweep that removes leftover s3-multipart-* directories
  older than the TTL, recovering part files from a previous crash
- Add a configurable s3.multipart_ttl (env S3_MULTIPART_TTL) duration,
  parsed via time.ParseDuration with a 24h default
- Add tests for TTL-based reaping and stale-directory cleanup

Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
@xrgzs
xrgzs force-pushed the feat/s3-multipart branch from 9a51b73 to 48f5af8 Compare August 7, 2026 01:14

@PIKACHUIM PIKACHUIM left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙏 感谢贡献

感谢 @xrgzs 提交此PR!我已完成代码评审,以下是评审结果。


📖 PR背景与需求

PR标题:feat(server/s3): support multipart upload

关联Issue:Related to #460

需求说明:为 OpenList 的 S3 服务器实现完整的分片上传(multipart upload)支持。此前 S3 分片上传会回退到 gofakes3 默认的内存缓冲器,导致大文件上传时可能耗尽内存;且客户端未完成的孤儿上传会一直占用磁盘空间。

预期目标

  1. 分片按流式方式落盘到临时文件,内存占用恒定
  2. 自动回收被遗弃的分片上传(超过TTL未活动)
  3. 兼容现有的单分片上传行为
  4. 支持 awscli / s3cmd / rclone 等标准 S3 客户端

📋 问题摘要

  • 功能性:完整实现了 S3 分片上传协议
  • 代码质量:测试覆盖充分,实现规范
  • ⚠️ 改进建议:有3处可优化点

📂 逐文件分析

go.mod & go.sum

改动意图:将依赖从 itsHenry35/gofakes3 v0.0.8 切换到 OpenListTeam/gofakes3 v0.8.0

代码修改逻辑

  • 旧版本只支持内存缓冲的默认上传器
  • OpenListTeam fork 新增了 MultipartBackend 接口,允许后端自行流式处理分片
  • 这是实现分片上传的前提

合理性评估

  • 优点:使用自己维护的 fork 可以快速修复问题和添加功能
  • ⚠️ 疑问:是否考虑过向上游 itsHenry35/gofakes3 贡献这个功能?长期维护 fork 会增加维护负担

internal/conf/config.go

改动意图:新增配置项 MultipartTTL,用于设置分片上传的过期时间。

代码修改逻辑

  • S3 结构体中新增 MultipartTTL string 字段
  • 支持通过环境变量 MULTIPART_TTL 配置
  • 默认值为 24 小时(在代码中处理)

合理性评估

  • 优点:可配置化设计,灵活性好
  • 优点:默认值合理(24小时)

详细建议
在注释中补充配置格式说明:

MultipartTTL string `json:"multipart_ttl" env:"MULTIPART_TTL"` // Duration string like "24h", "30m"; defaults to 24h if empty or invalid

server/s3/backend.go

改动意图:重构 s3Backend,添加分片上传支持。

代码修改逻辑

  1. 新增字段uploads *sync.Map 用于跟踪进行中的分片上传
  2. 提取共享逻辑:将 PutObject 的主体逻辑抽取为 putStream 函数,使其可以被单分片上传和分片完成路径共享
  3. 启动清理协程:在 newBackend() 中调用 startReaper() 启动后台清理协程

合理性评估

  • 优点putStream 的抽取避免了代码重复,确保单分片和多分片上传遵循相同的业务规则(目录创建、元数据、忽略规则)
  • 优点PutObject 的行为不变,向后兼容
  • ⚠️ 疑问startReaper() 启动的协程没有明确的停止机制,如果服务重启频繁可能会留下孤儿协程(虽然在实践中不太可能)

server/s3/multipart.go (新增,379行)

改动意图:实现 gofakes3.MultipartBackend 接口的四个核心方法。

代码修改逻辑

  1. CreateMultipartUpload

    • conf.Conf.TempDir 下创建 s3-multipart-* 临时目录
    • 生成 UUID 作为 uploadID
    • multipartState 存入 b.uploads map
  2. UploadPart

    • 验证分片号在有效范围内(1 到 MaxUploadPartNumber)
    • 将分片流式写入临时文件(part-XXXXX
    • 计算 MD5 并返回 quoted etag
    • 验证 Content-Length 与实际读取字节数匹配(防止截断)
    • 更新 lastActivity 时间戳
  3. CompleteMultipartUpload

    • 验证分片列表按升序排列
    • 验证每个分片存在且 etag 匹配
    • 按顺序打开所有分片文件,使用 io.MultiReader 拼接
    • 调用 putStream 将拼接结果写入底层存储
    • 计算并返回 S3 规范的 multipart etag:"<md5ofmd5s>-N"
    • 成功后清理临时文件和bookkeeping
  4. AbortMultipartUpload

    • 删除临时目录
    • b.uploads 中移除记录
    • 对未知 uploadID 保持幂等
  5. Reaper机制

    • 每个 backend 实例启动一个后台协程
    • TTL/4(钳制在 [10s, 1h])的间隔执行清理
    • 移除 lastActivity 超过 TTL 的上传
    • 启动时清理上次进程崩溃残留的 s3-multipart-* 目录

合理性评估

  • 优点:完全符合 S3 分片上传协议规范
  • 优点:短读检测(ErrIncompleteBody)防止截断数据被静默接受
  • 优点:Complete 失败时保留上传状态,允许客户端重试
  • 优点:Abort 的幂等性设计符合最佳实践
  • 优点:Reaper 机制自动清理孤儿上传,防止磁盘泄漏
  • ⚠️ 疑问io.MultiReader 打开所有分片文件后,如果 putStream 失败,这些文件会被正确关闭吗?

详细建议

  1. 资源清理保障
    当前代码在 Complete 中打开所有分片文件后,通过 utils.NewReadCloser 包装了一个统一的 Close 函数。但如果 putStream 内部发生 panic,这些文件可能不会被关闭。建议使用 defer 确保清理:
combined := utils.NewReadCloser(io.MultiReader(readers...), func() error {
    var firstErr error
    for _, c := range closers {
        if err := c.Close(); err != nil && firstErr == nil {
            firstErr = err
        }
    }
    return firstErr
})
defer combined.Close() // 确保无论如何都会关闭

err := b.putStream(ctx, bucket, object, state.meta, combined, total)
if err != nil {
    return "", "", err
}
  1. 并发安全说明
    代码注释提到"gofakes3 does not serialize multipart operations",建议在 multipartState 结构体定义处补充并发安全文档:
// multipartState tracks one in-progress multipart upload.
//
// Concurrency: gofakes3 does not serialize operations for the same uploadID.
// The parts map is protected by mu. Each part file is independent, so concurrent
// UploadPart calls for different part numbers are safe without additional locking.
type multipartState struct {
    // ...
}

server/s3/multipart_test.go (新增,321行)

改动意图:提供完整的分片上传功能测试覆盖。

代码修改逻辑

  • TestMultipartUploadEndToEnd:端到端测试,覆盖创建、上传3个分片、完成、验证结果文件内容和etag
  • 还覆盖了错误场景:短读、未知uploadID、越界分片号、乱序完成、错误etag、缺失分片、失败的complete保留上传
  • TestMultipartAbort:测试abort删除临时目录和幂等性
  • TestMultipartReapExpired:测试TTL超时的上传被回收,活跃上传保留
  • TestMultipartCleanupStaleDirs:测试启动时清理残留目录

合理性评估

  • 优点:测试覆盖非常全面,包含正常流程和多种边界情况
  • 优点:使用真实的 Local 驱动进行端到端测试,而非 mock
  • 优点:测试用例有清晰的 setup/cleanup 逻辑

其他文件修改

改动意图:将所有文件中的 github.com/itsHenry35/gofakes3 import 路径替换为 github.com/OpenListTeam/gofakes3

涉及文件

  • server/s3/list.go
  • server/s3/logger.go
  • server/s3/pager.go
  • server/s3/redirect.go
  • server/s3/server.go
  • server/s3/utils.go

合理性评估

  • 优点:更新彻底,没有遗漏

🎯 总体评价

功能性:⭐⭐⭐⭐⭐ - 完整实现了 S3 分片上传协议,解决了内存和磁盘泄漏问题
安全性:⭐⭐⭐⭐⭐ - 输入验证充分,短读检测到位,无明显安全隐患
代码质量:⭐⭐⭐⭐⭐ - 代码结构清晰,测试覆盖完善,注释详尽
实现方案:⭐⭐⭐⭐⭐ - 流式处理分片避免内存峰值,Reaper机制优雅,资源管理到位

建议操作

  • Approve(强烈建议合并)
  • 🔄 Request Changes(需要修改)
  • ❌ Close(建议关闭)

理由:此 PR 是一个非常高质量的功能实现,完全解决了 S3 服务的关键痛点:

  1. 将大文件上传的内存占用从"与文件大小成正比"降至"恒定"
  2. 通过 Reaper 机制自动清理孤儿上传,防止磁盘泄漏
  3. 向后兼容现有单分片上传行为
  4. 测试覆盖全面(包括端到端、边界情况、错误处理)
  5. 代码质量极高(结构清晰、注释详尽、符合最佳实践)

建议的改进点(资源清理的 defer、注释补充)都是锦上添花,不阻碍合并。


💡 后续建议

  1. 文档更新:在 OpenList-Docs 中补充分片上传功能说明和 MULTIPART_TTL 配置项
  2. 监控指标:考虑添加 Prometheus 指标,监控活跃上传数、清理的孤儿上传数等
  3. 上游贡献:如果时间允许,考虑向 itsHenry35/gofakes3 贡献 MultipartBackend 接口,减少长期维护 fork 的负担
  4. 性能测试:在生产环境监控分片上传的性能表现(延迟、吞吐量、磁盘IO)

再次感谢你的精彩贡献!这是一个教科书级别的功能实现。👏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ecosystem Module: Server API and protocol changes Module: Stream Transmission optimization and file stream handling-related features WIP An Issue already has a PR to fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants